Skip to content

Initial implementation of simradio-based testing with a multi-node simulated mesh#946

Open
ianmcorvidae wants to merge 8 commits into
meshtastic:masterfrom
ianmcorvidae:sim-based-testing
Open

Initial implementation of simradio-based testing with a multi-node simulated mesh#946
ianmcorvidae wants to merge 8 commits into
meshtastic:masterfrom
ianmcorvidae:sim-based-testing

Conversation

@ianmcorvidae

@ianmcorvidae ianmcorvidae commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

When merged this should fix #643 as well. Still adding a bit of stuff to make smokevirt not so useless.

Summary by CodeRabbit

  • New Features
    • Added new LoRa modem preset CLI options for channel preset switching.
    • Added expanded simulator smoke coverage for multi-node delivery, relaying, hop limits, node discovery, and traceroutes.
  • Bug Fixes
    • Improved channel deletion to reduce unnecessary firmware writes by updating only changed channel entries.
  • Tests
    • Enhanced simulator firmware harness and smoke helpers; refactored simulator CLI tests to validate persisted configuration state.
    • Updated pytest markers and CI to run additional simulator coverage across multiple release channels.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a meshtasticd-based simulator test harness, shared helpers, fixtures, smoke tests, and CI/pytest wiring. It also changes Node.deleteChannel to write only modified channel slots and adds unit coverage.

Changes

Firmware Simulator Test Infrastructure

Layer / File(s) Summary
CI and pytest wiring
.github/workflows/ci.yml, pytest.ini
Adds the simulator smoke-test CI job, updates Python matrix and checkout settings, and registers the smokemesh marker in pytest configuration.
Simulator harness core
meshtastic/tests/firmware_harness.py
Introduces simulator discovery, host checks, port readiness, per-node meshtasticd lifecycle, multi-node orchestration, topology-based packet forwarding, convergence waiting, and MeshPacket reconstruction.
Shared CLI and receive helpers
meshtastic/tests/fw_helpers.py
Adds CLI execution, fresh-interface verification, packet collection, topic subscriptions, and pubsub cleanup helpers for simulator-driven smoke tests.
Firmware fixtures
meshtastic/tests/conftest.py
Adds firmware availability skipping and fixtures that start, yield, and stop single-node and three-node SimMesh instances.
Single-node and multi-node smoke tests
meshtastic/tests/test_smokevirt.py, meshtastic/tests/test_smokemesh.py
Reworks the single-node smoke suite around simulator fixtures and protobuf-backed assertions, and adds multi-node mesh tests for delivery, relaying, hop limits, node listing, and traceroutes.

Estimated code review effort: 4 (Complex) | ~75 minutes

Node.deleteChannel Write Optimization

Layer / File(s) Summary
deleteChannel diff-based writes
meshtastic/node.py, meshtastic/tests/test_node.py
deleteChannel now compares serialized channel state before and after shifting and writes only changed indices, with tests covering the changed suffix and primary-channel rejection.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant pytest
  participant meshtasticd
  GitHubActions->>pytest: run smokevirt or smokemesh
  pytest->>meshtasticd: start simulator-based tests
  pytest-->>GitHubActions: report results
Loading

Related Issues: #643

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main change: initial simradio-based multi-node simulated mesh testing.
Linked Issues check ✅ Passed The PR implements real firmware-backed TCP integration tests using meshtasticd simulator instances, matching the requested device-like Python integration approach.
Out of Scope Changes check ✅ Passed The extra code changes support the new simulator-driven tests and do not appear unrelated to the integration-testing goal.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.38%. Comparing base (7a7353c) to head (7950798).

Files with missing lines Patch % Lines
meshtastic/__main__.py 66.66% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #946      +/-   ##
==========================================
+ Coverage   66.84%   67.38%   +0.53%     
==========================================
  Files          25       25              
  Lines        4741     4752      +11     
==========================================
+ Hits         3169     3202      +33     
+ Misses       1572     1550      -22     
Flag Coverage Δ
unittests 67.38% <76.92%> (+0.53%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ianmcorvidae ianmcorvidae marked this pull request as ready for review July 6, 2026 19:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
meshtastic/tests/test_smokevirt.py (2)

244-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused helper _set_and_verify.

This wrapper around cli_then_verify is defined but not called anywhere in the file; all tests call cli_then_verify directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meshtastic/tests/test_smokevirt.py` around lines 244 - 246, The helper
_set_and_verify is unused and duplicates cli_then_verify, so remove the dead
wrapper from the test file and update any remaining references if needed to call
cli_then_verify directly. If you want to keep it for future use, add a real
caller; otherwise delete the _set_and_verify function to avoid stale, misleading
test helpers.

542-559: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: prefer unpacking over list concatenation.

Ruff flags ["--pos-fields"] + _POS_FIELDS_INPUT (line 558); [*_POS_FIELDS_INPUT]-style unpacking is the idiomatic form.

Suggested fix
     cli_then_verify(
         firmware_node.port,
-        ["--pos-fields"] + _POS_FIELDS_INPUT,
+        ["--pos-fields", *_POS_FIELDS_INPUT],
         check,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meshtastic/tests/test_smokevirt.py` around lines 542 - 559, The smoke test in
test_smokevirt_pos_fields builds the CLI arguments with list concatenation,
which Ruff prefers to be unpacked idiomatically. Update the cli_then_verify call
in test_smokevirt_pos_fields to pass the --pos-fields arguments using unpacking
with _POS_FIELDS_INPUT instead of ["--pos-fields"] + _POS_FIELDS_INPUT, keeping
the rest of the test logic unchanged.

Source: Linters/SAST tools

.github/workflows/ci.yml (1)

84-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated poetry setup vs the validate job.

The poetry install/version-check steps mirror the validate job almost verbatim (differing only by --all-extras --with dev). Consider extracting a reusable composite action or workflow template if more jobs adopt this pattern, though it's not urgent for two jobs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 84 - 95, The Python/Poetry setup in
the CI workflow duplicates the same install/version-check flow used by the
validate job, so consolidate the shared setup behind a reusable workflow,
composite action, or a single common step sequence. Update the job that runs the
meshtastic install to reuse that shared logic and keep only the job-specific
Poetry options such as all-extras and dev, using the existing validate job and
its Poetry install steps as the reference points.
meshtastic/tests/test_smokemesh.py (1)

94-99: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

test_smokemesh_show_nodes makes no assertions.

The test only invokes showNodes() on each interface without validating output content, so it only verifies the call doesn't raise — it wouldn't catch missing/incorrect node entries.

♻️ Suggested improvement using capsys
 `@pytest.mark.smokemesh`
-def test_smokemesh_show_nodes(firmware_mesh):
+def test_smokemesh_show_nodes(firmware_mesh, capsys):
     """showNodes should report the other nodes in the mesh."""
     for i in range(3):
         iface = firmware_mesh.get_iface(i)
         iface.showNodes()
+        out = capsys.readouterr().out
+        assert out.strip(), f"node {i} showNodes produced no output"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meshtastic/tests/test_smokemesh.py` around lines 94 - 99,
test_smokemesh_show_nodes only calls showNodes() and never verifies behavior, so
add assertions to confirm the printed output contains the expected node
information. Update the test to use capsys around
firmware_mesh.get_iface(i).showNodes() and assert the captured output includes
the other mesh nodes, so the test validates content rather than just absence of
exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 84-89: The checkout step in the CI workflow is persisting
credentials by default, which should be disabled for this job. Update the
actions/checkout usage in the workflow to set persist-credentials to false,
keeping the token from being available after checkout while the job continues
with the PPA and meshtasticd installation steps.

In `@meshtastic/tests/conftest.py`:
- Around line 34-65: SimMesh startup can fail after some meshtasticd processes
are already launched, leaving orphaned nodes because SimMesh.stop() exits early
before _started is set. Update SimMesh.start() and/or the startup path in
firmware_harness.py to track partially started nodes and invoke cleanup on any
failure from connect() or later initialization steps. Use the SimMesh.start,
SimMesh.stop, and connect flow to ensure already-started processes are stopped
even when startup does not complete.

In `@meshtastic/tests/firmware_harness.py`:
- Around line 193-212: Partial startup failures in
`firmware_harness.FirmwareMesh.start` leave previously launched nodes running
because `_started` stays false and `stop()` won’t clean them up. Wrap the
`node.start()`/`node.connect()` setup in `start()` with failure handling that
stops any nodes already started and clears their resources before re-raising.
Use the existing `FirmwareMesh.start`, `FirmwareMesh.stop`, and node lifecycle
methods to ensure partial initialization is always torn down.

In `@meshtastic/tests/test_node.py`:
- Around line 729-742: The test in Node.deleteChannel’s primary-slot rejection
path uses an equality check for the exception type, which triggers Ruff E721.
Update the assertion in test_delete_channel_rejects_primary to compare
pytest_wrapped_e.type to SystemExit with identity semantics instead of equality,
keeping the rest of the SystemExit validation unchanged.

In `@meshtastic/tests/test_smokemesh.py`:
- Around line 102-128: The smokemesh traceroute test is using a fixed sleep and
an unguarded traceroute payload lookup. In
test_smokemesh_traceroute_across_relay, replace the time.sleep delay with
PacketCollector.wait_for(1) so the test waits deterministically for traceroute
packets, and update the a_resp decoded traceroute access to use a guarded
assertion-friendly lookup so missing payloads fail through the test assertion
rather than a KeyError.

In `@meshtastic/tests/test_smokevirt.py`:
- Around line 394-432: `test_smokevirt_seturl_default` currently bypasses the
`--seturl` CLI path by calling `iface.localNode.setURL` directly, so it does not
test argument parsing or CLI behavior. Update the test to use `run_cli` for the
URL mutation, then reconnect or use a fresh interface to verify the resulting
URL via `firmware_node`/`iface.localNode.getURL`, keeping the existing
payload-decoding assertions intact.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 84-95: The Python/Poetry setup in the CI workflow duplicates the
same install/version-check flow used by the validate job, so consolidate the
shared setup behind a reusable workflow, composite action, or a single common
step sequence. Update the job that runs the meshtastic install to reuse that
shared logic and keep only the job-specific Poetry options such as all-extras
and dev, using the existing validate job and its Poetry install steps as the
reference points.

In `@meshtastic/tests/test_smokemesh.py`:
- Around line 94-99: test_smokemesh_show_nodes only calls showNodes() and never
verifies behavior, so add assertions to confirm the printed output contains the
expected node information. Update the test to use capsys around
firmware_mesh.get_iface(i).showNodes() and assert the captured output includes
the other mesh nodes, so the test validates content rather than just absence of
exceptions.

In `@meshtastic/tests/test_smokevirt.py`:
- Around line 244-246: The helper _set_and_verify is unused and duplicates
cli_then_verify, so remove the dead wrapper from the test file and update any
remaining references if needed to call cli_then_verify directly. If you want to
keep it for future use, add a real caller; otherwise delete the _set_and_verify
function to avoid stale, misleading test helpers.
- Around line 542-559: The smoke test in test_smokevirt_pos_fields builds the
CLI arguments with list concatenation, which Ruff prefers to be unpacked
idiomatically. Update the cli_then_verify call in test_smokevirt_pos_fields to
pass the --pos-fields arguments using unpacking with _POS_FIELDS_INPUT instead
of ["--pos-fields"] + _POS_FIELDS_INPUT, keeping the rest of the test logic
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: db37b9e9-10fb-4ad6-88db-88ae9c7eee05

📥 Commits

Reviewing files that changed from the base of the PR and between 7a7353c and ceca7b3.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • meshtastic/node.py
  • meshtastic/tests/conftest.py
  • meshtastic/tests/firmware_harness.py
  • meshtastic/tests/fw_helpers.py
  • meshtastic/tests/test_node.py
  • meshtastic/tests/test_smokemesh.py
  • meshtastic/tests/test_smokevirt.py
  • pytest.ini

Comment thread .github/workflows/ci.yml
Comment on lines +34 to +65
@pytest.fixture(scope="function")
def firmware_node():
"""A single meshtasticd sim node for smokevirt tests.

Function-scoped so every test gets a freshly-erased node with no
state leaking from previous tests. This makes destructive commands
(``--reboot``, ``--set factory_reset true``) safe to run and lets
tests be order-independent.

Yields the SimNode instance. The node is booted with a fresh erased
config and listens on localhost at its TCP port.
"""
_skip_firmware_if_unavailable()
mesh = SimMesh(n_nodes=1, base_port=SINGLE_NODE_BASE_PORT)
mesh.start()
yield mesh.get_node(0)
mesh.stop()


@pytest.fixture(scope="function")
def firmware_mesh():
"""A 3-node chain (A-B-C) meshtasticd sim mesh for smokemesh tests.

Yields the SimMesh instance. Nodes are connected and the SIMULATOR_APP
packet bridge is running. Node DB convergence is awaited.
"""
_skip_firmware_if_unavailable()
mesh = SimMesh(n_nodes=3, topology=CHAIN_TOPOLOGY)
mesh.start()
mesh.wait_for_convergence(timeout=30)
yield mesh
mesh.stop()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect SimMesh.start/stop and SimNode.start/close for partial-failure cleanup handling
fd firmware_harness.py meshtastic/tests --exec cat -n {}

Repository: meshtastic/python

Length of output: 14743


Handle partial startup cleanup in SimMesh.start()
mesh.start() can fail after some meshtasticd processes are already running, but SimMesh.stop() returns early until _started is set. Clean up already-started nodes inside firmware_harness.py so a failed connect() or later startup step doesn’t leave orphaned processes behind.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meshtastic/tests/conftest.py` around lines 34 - 65, SimMesh startup can fail
after some meshtasticd processes are already launched, leaving orphaned nodes
because SimMesh.stop() exits early before _started is set. Update
SimMesh.start() and/or the startup path in firmware_harness.py to track
partially started nodes and invoke cleanup on any failure from connect() or
later initialization steps. Use the SimMesh.start, SimMesh.stop, and connect
flow to ensure already-started processes are stopped even when startup does not
complete.

Comment on lines +193 to +212
def start(self) -> None:
"""Launch all nodes, connect, and start the packet bridge."""
binary = find_meshtasticd()
if binary is None:
raise RuntimeError(
"meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH."
)

for node in self.nodes:
node.start(binary)

for node in self.nodes:
node.connect()
self._port_to_idx[node.port] = node.node_id

pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
self._started = True

if self.n_nodes > 1:
self._trigger_convergence()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Partial startup failure leaks meshtasticd processes.

If node.start() (or node.connect()) raises after earlier nodes have already launched, the exception propagates with self._started still False. The firmware_mesh/firmware_node fixtures only call mesh.stop() after yield, and stop() early-returns when _started is False, so the already-launched simulator processes (and their temp dirs) are never cleaned up and become orphans that also hold onto the TCP ports.

Wrap startup so any already-started nodes are torn down before re-raising.

🔧 Proposed cleanup on failure
-        for node in self.nodes:
-            node.start(binary)
-
-        for node in self.nodes:
-            node.connect()
-            self._port_to_idx[node.port] = node.node_id
-
-        pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
-        self._started = True
+        try:
+            for node in self.nodes:
+                node.start(binary)
+
+            for node in self.nodes:
+                node.connect()
+                self._port_to_idx[node.port] = node.node_id
+        except Exception:
+            for node in self.nodes:
+                node.close()
+            raise
+
+        pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
+        self._started = True
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def start(self) -> None:
"""Launch all nodes, connect, and start the packet bridge."""
binary = find_meshtasticd()
if binary is None:
raise RuntimeError(
"meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH."
)
for node in self.nodes:
node.start(binary)
for node in self.nodes:
node.connect()
self._port_to_idx[node.port] = node.node_id
pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
self._started = True
if self.n_nodes > 1:
self._trigger_convergence()
def start(self) -> None:
"""Launch all nodes, connect, and start the packet bridge."""
binary = find_meshtasticd()
if binary is None:
raise RuntimeError(
"meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH."
)
try:
for node in self.nodes:
node.start(binary)
for node in self.nodes:
node.connect()
self._port_to_idx[node.port] = node.node_id
except Exception:
for node in self.nodes:
node.close()
raise
pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
self._started = True
if self.n_nodes > 1:
self._trigger_convergence()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meshtastic/tests/firmware_harness.py` around lines 193 - 212, Partial startup
failures in `firmware_harness.FirmwareMesh.start` leave previously launched
nodes running because `_started` stays false and `stop()` won’t clean them up.
Wrap the `node.start()`/`node.connect()` setup in `start()` with failure
handling that stops any nodes already started and clears their resources before
re-raising. Use the existing `FirmwareMesh.start`, `FirmwareMesh.stop`, and node
lifecycle methods to ensure partial initialization is always torn down.

Comment thread meshtastic/tests/test_node.py
Comment on lines +102 to +128
@pytest.mark.smokemesh
def test_smokemesh_traceroute_across_relay(firmware_mesh):
"""Traceroute from A to C should show route via B in both directions."""
col_a = subscribe_traceroutes(firmware_mesh.get_iface(0))
col_c = subscribe_traceroutes(firmware_mesh.get_iface(2))
try:
src_a = firmware_mesh.get_node(0).node_num
dest_c = firmware_mesh.get_node(2).node_num
node_b = firmware_mesh.get_node(1).node_num

firmware_mesh.get_iface(0).sendTraceRoute(dest=dest_c, hopLimit=3)

time.sleep(2)

assert len(col_a.traceroutes) >= 1, "A did not receive traceroute response"
a_resp = col_a.traceroutes[0]
assert a_resp["from"] == dest_c, "response source should be C"

route = a_resp["decoded"]["traceroute"]
assert route.get("route") == [node_b], "forward route should be A→B→C"
assert route.get("routeBack") == [node_b], "return route should be C→B→A"

assert len(col_c.traceroutes) >= 1, "C did not receive traceroute request"
c_req = col_c.traceroutes[0]
assert c_req["from"] == src_a, "request source should be A"
finally:
unsubscribe_all("meshtastic.receive.traceroute")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect PacketCollector.wait_for signature and traceroute bridging in the harness
# to confirm whether wait_for works for traceroute topics and where the TRACEROUTE_APP
# packet bridging is implemented.
rg -n -A10 'def wait_for' meshtastic/tests/fw_helpers.py
rg -n -B2 -A15 'TRACEROUTE_APP|RouteDiscovery' meshtastic/tests/firmware_harness.py

Repository: meshtastic/python

Length of output: 639


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate traceroute collector helpers and related tests.
rg -n -A6 -B4 'subscribe_traceroutes|traceroutes|wait_for\(' meshtastic/tests
printf '\n--- firmware_harness traceroute handling ---\n'
rg -n -A8 -B4 'TRACEROUTE_APP|RouteDiscovery|traceroute' meshtastic/tests/firmware_harness.py
printf '\n--- test_smokemesh context ---\n'
sed -n '1,220p' meshtastic/tests/test_smokemesh.py

Repository: meshtastic/python

Length of output: 9159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A12 -B6 'def sendTraceRoute|waitForTraceRoute|send_trace_route' meshtastic/tests fw_helpers.py meshtastic/tests/firmware_harness.py

Repository: meshtastic/python

Length of output: 2363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A8 -B4 'decoded"\]\["traceroute"|get\("decoded", \{\}\)\.get\("traceroute"|decoded.*traceroute' meshtastic/tests

Repository: meshtastic/python

Length of output: 1324


Replace the fixed sleep with wait_for(1) here. PacketCollector already supports deterministic polling for traceroute packets, and a_resp["decoded"]["traceroute"] should use a guarded lookup so a missing payload fails with the assertion instead of a KeyError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meshtastic/tests/test_smokemesh.py` around lines 102 - 128, The smokemesh
traceroute test is using a fixed sleep and an unguarded traceroute payload
lookup. In test_smokemesh_traceroute_across_relay, replace the time.sleep delay
with PacketCollector.wait_for(1) so the test waits deterministically for
traceroute packets, and update the a_resp decoded traceroute access to use a
guarded assertion-friendly lookup so missing payloads fail through the test
assertion rather than a KeyError.

Source: Pipeline failures

Comment on lines 394 to 432
@pytest.mark.smokevirt
def test_smokevirt_ensure_ch_del_second_of_three_channels():
"""Test that when we delete the 2nd of 3 channels, that it deletes the correct channel."""
return_value, out = subprocess.getstatusoutput(
"meshtastic --host localhost --ch-add testing1"
)
assert re.match(r"Connected to radio", out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info")
assert re.match(r"Connected to radio", out)
assert re.search(r"SECONDARY", out, re.MULTILINE)
assert re.search(r"testing1", out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput(
"meshtastic --host localhost --ch-add testing2"
)
assert re.match(r"Connected to radio", out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info")
assert re.match(r"Connected to radio", out)
assert re.search(r"testing2", out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput(
"meshtastic --host localhost --ch-del --ch-index 1"
)
assert re.match(r"Connected to radio", out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info")
assert re.match(r"Connected to radio", out)
assert re.search(r"testing2", out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput(
"meshtastic --host localhost --ch-del --ch-index 1"
)
assert re.match(r"Connected to radio", out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
def test_smokevirt_seturl_default(firmware_node):
"""--seturl applies a known channel URL."""
url = "https://www.meshtastic.org/d/#CgUYAyIBAQ"

# Use the fixture's already-connected TCPInterface so the same
# connection handles the firmware restart after setURL.
if firmware_node.iface is None:
pytest.fail("fixture interface not connected")
iface = firmware_node.iface
iface.localNode.setURL(url)

@pytest.mark.smokevirt
def test_smokevirt_ensure_ch_del_third_of_three_channels():
"""Test that when we delete the 3rd of 3 channels, that it deletes the correct channel."""
return_value, out = subprocess.getstatusoutput(
"meshtastic --host localhost --ch-add testing1"
)
assert re.match(r"Connected to radio", out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info")
assert re.match(r"Connected to radio", out)
assert re.search(r"SECONDARY", out, re.MULTILINE)
assert re.search(r"testing1", out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput(
"meshtastic --host localhost --ch-add testing2"
)
assert re.match(r"Connected to radio", out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info")
assert re.match(r"Connected to radio", out)
assert re.search(r"testing2", out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput(
"meshtastic --host localhost --ch-del --ch-index 2"
)
assert re.match(r"Connected to radio", out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info")
assert re.match(r"Connected to radio", out)
assert re.search(r"testing1", out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput(
"meshtastic --host localhost --ch-del --ch-index 1"
time.sleep(2.0)

actual = iface.localNode.getURL()
assert "meshtastic.org" in actual, f"not a channel URL: {actual}"
# The firmware may reshape the URL and use slightly different base64
# for the same data (trailing bits are ignored during decode), so we
# decode both and compare the protobuf payload prefix rather than
# doing a substring match on the encoded form.
_, _, frag = actual.partition("/#")
if not frag:
_, _, frag = actual.rpartition("#")
missing = len(frag) % 4
if missing:
frag += "=" * (4 - missing)
actual_bytes: bytes = base64.urlsafe_b64decode(frag)

_, _, efrag = url.partition("/#")
missing = len(efrag) % 4
if missing:
efrag += "=" * (4 - missing)
expected_bytes: bytes = base64.urlsafe_b64decode(efrag)

assert actual_bytes.startswith(expected_bytes), (
f"URL payload mismatch:\n"
f" expected (hex): {expected_bytes.hex()}\n"
f" actual (hex): {actual_bytes.hex()}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

test_smokevirt_seturl_default never exercises the --seturl CLI flag.

The test's name/docstring claims to test --seturl, but it calls iface.localNode.setURL(url) directly on the fixture's Python interface — the CLI is never invoked. This means CLI argument parsing/handling for --seturl (which is explicitly tested via run_cli for the invalid-URL case right below) is not covered here, undermining the smoke-test's stated purpose of exercising the real CLI path against the simulator.

Suggested fix: invoke `run_cli` for the mutation, then verify via a fresh interface (consistent with the rest of the file)
 `@pytest.mark.smokevirt`
 def test_smokevirt_seturl_default(firmware_node):
     """--seturl applies a known channel URL."""
     url = "https://www.meshtastic.org/d/#CgUYAyIBAQ"
 
-    # Use the fixture's already-connected TCPInterface so the same
-    # connection handles the firmware restart after setURL.
-    if firmware_node.iface is None:
-        pytest.fail("fixture interface not connected")
-    iface = firmware_node.iface
-    iface.localNode.setURL(url)
-
-    time.sleep(2.0)
-
-    actual = iface.localNode.getURL()
+    rc, out = run_cli(firmware_node.port, "--seturl", url)
+    assert rc == 0, out
+
+    def check(iface):
+        actual = iface.localNode.getURL()
+        ...
+    verify_state(firmware_node.port, check)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meshtastic/tests/test_smokevirt.py` around lines 394 - 432,
`test_smokevirt_seturl_default` currently bypasses the `--seturl` CLI path by
calling `iface.localNode.setURL` directly, so it does not test argument parsing
or CLI behavior. Update the test to use `run_cli` for the URL mutation, then
reconnect or use a fresh interface to verify the resulting URL via
`firmware_node`/`iface.localNode.getURL`, keeping the existing payload-decoding
assertions intact.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@meshtastic/__main__.py`:
- Around line 972-980: Fix the incorrect protobuf accessor in the channel preset
handling inside __main__.py: the LONG_MODERATE and LONG_TURBO branches are using
config_pb2.config instead of the valid config_pb2.Config used by the neighboring
preset cases. Update the setSimpleConfig calls in that block so they all
reference config_pb2.Config.LoRaConfig.ModemPreset consistently, matching the
smoke test expectations and avoiding AttributeError for the affected CLI flags.
- Line 1972: The help text for the modem preset option contains a typo in the
deprecation message. Update the string in the argument definition for the
VERY_LONG_SLOW preset so it says “Deprecated since 2.5 firmware.” instead of
“Deprecated sincce 2.5 firmware.”
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bf9ca07f-07f4-45d3-99fc-6bae0c62bbc5

📥 Commits

Reviewing files that changed from the base of the PR and between ceca7b3 and bffaff1.

📒 Files selected for processing (2)
  • meshtastic/__main__.py
  • meshtastic/tests/test_smokevirt.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • meshtastic/tests/test_smokevirt.py

Comment thread meshtastic/__main__.py
Comment thread meshtastic/__main__.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)

74-106: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a job-level timeout to bound the whole simradio_testing job.

Only the final pytest step has timeout-minutes: 15; the PPA add/update/install and poetry install steps are unbounded. A hung apt-get update/PPA fetch (network flakiness against a third-party PPA) could tie up a runner well past the intended time budget.

⏱️ Proposed fix
   simradio_testing:
     runs-on: ubuntu-latest
+    timeout-minutes: 25
     strategy:
       fail-fast: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 74 - 106, Add a job-level timeout to
the simradio_testing workflow job so the entire matrix run is bounded, not just
the final pytest step. Update the simradio_testing job definition in the CI
workflow to enforce an overall timeout that covers the checkout, setup-python,
poetry install, and meshtasticd PPA install steps, while keeping the existing
Run firmware smoke tests step timeout as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 74-106: Add a job-level timeout to the simradio_testing workflow
job so the entire matrix run is bounded, not just the final pytest step. Update
the simradio_testing job definition in the CI workflow to enforce an overall
timeout that covers the checkout, setup-python, poetry install, and meshtasticd
PPA install steps, while keeping the existing Run firmware smoke tests step
timeout as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cf0571f-d7b0-4d62-8d64-691f55323075

📥 Commits

Reviewing files that changed from the base of the PR and between bffaff1 and 7950798.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • meshtastic/__main__.py
  • meshtastic/tests/test_node.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • meshtastic/tests/test_node.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use a precompiled Portduino version of firmware for python integration testing?

1 participant